Skip to main content

News Provider

Provider supplies data about news related to a specific trading instrument.

/**
* Interface for receiving news data
*
* When loading dxCharts library chart, news events are taken from this interface
*
* When connecting dxCharts library, developer can implement this interface or use the default implementation [com.devexperts.dxcharts.provider.news.DxFeedNewsProvider] and pass it to the library using [DxChartsDataProviders] data class
*
* Use [dataFlow] to get news data as list of [NewsData.NewsItem]
*
* Use [changeSymbol] to change news instrument symbol
*/
interface DxChartsNewsProvider {
/**
* Flow of receiving news data
*
* News data is represented by list of [NewsData.NewsItem]
*/
val dataFlow: StateFlow<List<NewsData.NewsItem>>
/**
* Changes news instrument symbol
*
* @param symbol instrument symbol
*/
fun changeSymbol(symbol: String)
}

Method: changeSymbol

This method is used to change the symbol of the instrument for which the news data is provided. It takes the following parameter:

  • symbol: The symbol of the selected instrument. A symbol is a unique identifier used for trading on the exchange. For example, the symbol for Apple stocks on NASDAQ is "AAPL".

Example

Here is an example of how to use the changeSymbol method:

newsProvider.changeSymbol("AAPL")

Data is sent by updating the state of the dataFlow variable. It is represented as a list of com.devexperts.dxcharts.provider.domain.NewsData.NewsItem objects, which stores data about the news of the selected instrument:

/**
* Data class for storing data about a single news item.
*
* @param title Title of news item that will be shown in the chart (f.e. Apple Inc. (AAPL) Q4 2021 Earnings Call Transcript)
* @param timestamp Timestamp in milliseconds of news item
* @param source String with url of the news. If null, the news item will not be clickable. If not null, the news item will be clickable and will open the url in the browser
*/
data class NewsItem(
val title: String,
val timestamp: Long,
val source: String?,
)

News items are displayed on the chart:

news_dot

news_card

Upon clicking the news card, there is a redirection to an external URL with the parameter NewsItem.source.

Here is the default implementation of DxChartsNewsProvider:

/**
* Default implementation of [DxChartsNewsProvider].
*
* Uses dxFeed API to get demo news data.
*
* Limit for list news - 20 items [NEWS_LIMIT]
*
* Uses the DxFeed api via Http requests using [OkHttpClient].
*
* A token [token] is used for authorization with Base64 encoding.
*
* When loading dxCharts library and starting the provider ([changeSymbol]), a requests are sent with the current symbol of the instrument ([symbol]).
*
* Query responses results are parsed with [parseData] method and put into [NewsData] objects.
*
* Requests occurs once every 10 seconds ([UPDATE_DELAY]).
* If an error occurs during the request, it is repeated every 500 milliseconds ([REQUEST_DELAY]).
*
* The data retrieval logic is executed in a loop in a separate thread using [ExecutorService] [executorService]. Loop is working until [connected] flag is true.
*
* Changing the symbol of the instrument and starting the work of the provider occurs using [changeSymbol] method - if the provider has already been started, then the thread with it stops, a new thread with a new symbol is started.
*
* Inside a separate thread [requester] method id executed - method that repeatedly executes queries and processes responses every [UPDATE_DELAY] ms.
*
* Inside [request] method there is a looped [requestNews] method that sends requests and parse responses.
*
* To manually stop the provider, you can use [disconnect] method. It stops the provider, cleans [executorService] and disconnects from dxFeed API.
*
* @property gson [Gson] object for parsing responses.
* @property executorService [ExecutorService] for executing requests in a separate thread.
* @property dataFlow public [StateFlow] with [List] of [NewsData.NewsItem] objects.
* @property _dataFlow internal [MutableStateFlow] with [List] of [NewsData.NewsItem] objects.
* @property client [OkHttpClient] for sending requests.
* @property request [Request] for sending requests with "Authorization" header with [token].
* @property loaded loaded flag that shows if data was successfully loaded in [requester] method.
* @property connected connected flag that shows if provider is connected to dxFeed API.
* @property symbol current symbol of selected instrument.
* @property _errorFlow Internal [MutableStateFlow] for sending errors.
* @property errorFlow [StateFlow] for sending errors.
*/
class DxFeedNewsProvider(
private val url: String = "",
private val token: String = "",
) : DxChartsNewsProvider, DxChartsErrorProvider<NewsProviderError> {
override val dataFlow: StateFlow<List<NewsData.NewsItem>> get() = _dataFlow
private val _dataFlow = MutableStateFlow<List<NewsData.NewsItem>>(emptyList())
private val gson = Gson()
private var executorService: ExecutorService? = null
private val client = OkHttpClient()
.newBuilder()
.build()
private val request = token.let { Request.Builder()
.addHeader("Authorization", it) }
private val _errorFlow = MutableStateFlow<NewsProviderError?>(null)
override val errorFlow: StateFlow<NewsProviderError?> get() = _errorFlow
@Volatile
private var loaded = false
@Volatile
private var connected = false
@Volatile
private var symbol: String? = null
/**
* Changes the symbol of the instrument and starts / restarts the work of the provider.
*
* If the provider and [executorService] are running, the method stops it (shutdownNow()).
*
* The passed parameter [symbol] is set to the value [symbol].
*
* [connected] flag sets to true
*
* The [requester] method is passed to [executorService] to run in a new thread.
*
* @value value New instrument symbol for requesting news
*/
override fun changeSymbol(symbol: String) {
executorService?.let {
if (!it.isShutdown) {
try {
it.shutdownNow()
} catch (_: Exception) {
}
}
}
this.symbol = symbol
_dataFlow.tryEmit(emptyList())
connected = true
executorService = Executors.newFixedThreadPool(1)
executorService?.execute {
requester(symbol)
}
}
/**
* Method in a loop executes the [requestNews] method every 10 seconds [UPDATE_DELAY] until [connected] == true
*
* At the beginning of the method, the [loaded] value is set to true
*
* If an error occurs when executing the [requestNews] method and [loaded] == false, then retries will be performed every 500 ms. [REQUEST_DELAY]
*/
private fun requester(symbol: String) {
try {
while (connected) {
loaded = false
while (!loaded) {
requestNews()
try {
Thread.sleep(REQUEST_DELAY)
} catch (e: InterruptedException) {
_errorFlow.tryEmit(NewsProviderError.InterruptionError("Requester loop interrupted by thread interruption"))
}
}
try {
Thread.sleep(UPDATE_DELAY)
} catch (e: InterruptedException) {
_errorFlow.tryEmit(NewsProviderError.InterruptionError("Requester loop interrupted by thread interruption"))
}
}
executorService?.shutdown()
} catch (e: Exception) {
Log.i(TAG, "Request for getting news with symbol $symbol has interrupted")
}
}
/**
* Method for manually stopping the provider, turning off [executorService].
*
* [connected] sets to false
*
* [loaded] sets to true
*/
fun disconnect() {
connected = false
loaded = true
try {
executorService?.shutdownNow()
} catch (_: Exception) {
}
}
/**
* Method for executing a request in dxFeed API to obtain a list of news based on the passed instrument symbol [symbol]
*
* Path for the request is calculating in the [urlWithSymbol] method where [symbol] is passed as an argument
*
* The request is made using [OkHttpClient] [client] via [request] with authorization token [token] in Base64 encoding
*
* If the request is successful, [loaded] flag is set to true, the data is parsed using [parseData] method and emitted to [dataFlow]
*/
private fun requestNews() {
val req = request.url(urlWithSymbol(symbol.orEmpty())).build()
val call = client.newCall(req)
try {
call.execute().use { response ->
when {
response.isSuccessful -> {
val body = response.body?.string() ?: return
val parsedData = body.parseData()
if (parsedData.isNotEmpty()) {
_dataFlow.tryEmit(parsedData)
loaded = true
} else {
_errorFlow.tryEmit(NewsProviderError.ParseError("No news items found"))
}
}
response.code == 401 -> {
_errorFlow.tryEmit(NewsProviderError.UnauthorizedError("Unauthorized: response.message"))
}
response.code in 500..599 -> {
_errorFlow.tryEmit(NewsProviderError.NetworkError("Server error: $response.message"))
}
else -> {
_errorFlow.tryEmit(NewsProviderError.UnknownError("Error: $response.message"))
}
}
}
} catch (e: Exception) {
_errorFlow.tryEmit(NewsProviderError.NetworkError("Failed to execute request: $e.message"))
}
}
/**
* Extension parses [String] data using [gson] into the list of [NewsData.NewsItem] objects
* @return parsed news data
*/
private fun String.parseData(): List<NewsData.NewsItem> {
val linkRegex = """<as+href="([^"]*)".*?>""".toRegex()
return try {
val dxFeedNewsData = gson.fromJson(this, DxFeedNewsData::class.java)
dxFeedNewsData.news.map { newsItem ->
val url = linkRegex.find(newsItem.body)?.groupValues?.getOrNull(1)
NewsData.NewsItem(
title = newsItem.title,
timestamp = newsItem.time.time,
source = url
)
}
} catch (e: Exception) {
Log.e(TAG, "Failed to parse news data", e)
_errorFlow.tryEmit(NewsProviderError.ParseError("Failed to parse news data: $e.message"))
emptyList()
}
}
/**
* @return Full URL Path to get news from dxFeed API with [url], [symbol] and [NEWS_LIMIT] params
*/
private fun urlWithSymbol(symbol: String) = "$url?symbol=$symbol&limit=$NEWS_LIMIT"
companion object {
const val TAG = "DxFeedNewsProvider"
const val NEWS_LIMIT = 20
const val REQUEST_DELAY = 500L
private const val UPDATE_DELAY = 10000L
}
}
/**
- Sealed class for representing different types of errors in DxFeedNewsProvider.
*/
sealed class NewsProviderError(override val message: String, override val error: Throwable?) : ProviderError {
data class NetworkError(
override val message: String,
override val error: Throwable? = null
) : NewsProviderError(message, error)
data class ParseError(
override val message: String,
override val error: Throwable? = null
) : NewsProviderError(message, error)
data class InterruptionError(
override val message: String,
override val error: Throwable? = null
) : NewsProviderError(message, error)
data class UnauthorizedError(
override val message: String,
override val error: Throwable? = null
) : NewsProviderError(message, error)
data class UnknownError(
override val message: String,
override val error: Throwable? = null
) : NewsProviderError(message, error)
}